Repository navigation
feat: add the GraphQL WebSocket client (G4, graphql-transport-ws) - #1594
Merged
Merged
Conversation
This was referenced Oct 30, 2026
jlouvel
marked this pull request as ready for review
October 30, 2026 15:13
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved cancellation and shutdown races can leak server operations and WebSocket connections, while decoding failures can leave subscribers waiting indefinitely.
7 open issues
Reader failures leave the WebSocket open without a reader · New Cancellation can send complete before subscribe · New Late channel registration can survive client shutdown · New Shutdown misses downstream subscriptions without active wire IDs · New Invalid demand is not handled or cleaned up promptly · New Setup failures leave subscribers unnotified and operations registered · New Pending delivery results are excluded from the buffer limit · New
What changed in this PR
Adds the client side of GraphQL G4: graphql-transport-ws subscriptions integrated with GraphqlClientAdapter. The PR also includes the prerequisite WebSocket lifecycle and shared-event-loop changes.
Changes:
- Adds multiplexed WebSocket subscriptions, connection initialization, demand-aware delivery, buffering limits, and cancellation.
- Lets WebSocket clients borrow the HTTP client’s Netty event loop without owning its lifecycle.
- Adds integration tests and release notes while keeping the WebSocket dependency optional.
| File | Description |
|---|---|
org.restlet/src/main/java/org/restlet/engine/netty/NettyClientHelper.java |
Exposes the connector’s event loop for extensions. |
org.restlet/src/main/java/org/restlet/Client.java |
Publishes the helper when the context is assigned. |
org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketTestCase.java |
Migrates setup to setter-based configuration. |
org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSharedEventLoopTestCase.java |
Tests borrowed event-loop ownership and validation. |
org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketSessionTestCase.java |
Updates configuration tests and checks lifecycle restrictions. |
org.restlet.ext.websocket/src/test/java/org/restlet/ext/websocket/WebSocketCompressionTestCase.java |
Migrates compression tests to the new client API. |
org.restlet.ext.websocket/src/main/java/org/restlet/ext/websocket/WebSocketClient.java |
Adds setters, lifecycle methods, and HTTP-client borrowing. |
org.restlet.ext.websocket/src/main/java/org/restlet/ext/websocket/internal/NettyWebSocketConnector.java |
Supports shared event loops and tracks connections for shutdown. |
org.restlet.ext.graphql/src/test/java/org/restlet/ext/graphql/GraphqlWebSocketClientTestCase.java |
Tests multiplexing, demand, cancellation, limits, and failures. |
org.restlet.ext.graphql/src/main/java/org/restlet/ext/graphql/GraphqlWebSocketTransport.java |
Implements the GraphQL WebSocket client protocol. |
org.restlet.ext.graphql/src/main/java/org/restlet/ext/graphql/GraphqlSubscriptionTransport.java |
Isolates the optional subscription transport. |
org.restlet.ext.graphql/src/main/java/org/restlet/ext/graphql/GraphqlClientAdapter.java |
Adds transport selection and connection-init configuration. |
changes.md |
Documents GraphQL client support and connector changes. |
Review effort: Balanced
jlouvel
force-pushed
the
feat/graphql-g4-websocket-server
branch
from
October 31, 2026 06:18
ab5539d to
63ed523
Compare
jlouvel
force-pushed
the
feat/graphql-g4-websocket-client
branch
2 times, most recently
from
October 31, 2026 07:32
0f851b5 to
50d00cb
Compare
jlouvel
force-pushed
the
feat/graphql-g4-websocket-server
branch
from
November 1, 2026 14:23
63ed523 to
150d94b
Compare
jlouvel
force-pushed
the
feat/graphql-g4-websocket-client
branch
2 times, most recently
from
November 1, 2026 17:10
15d31b2 to
654387e
Compare
jlouvel
force-pushed
the
feat/graphql-g4-websocket-server
branch
from
November 1, 2026 20:09
8db6b37 to
3f292a6
Compare
- GraphqlClientAdapter.setSubscriptionTransport(WEBSOCKET): subscribe() multiplexes operations over one graphql-transport-ws connection, opened on the first demand at the endpoint with the ws or wss scheme. - The WebSocketClient borrows the event loop of the adapter's HTTP client (WebSocketClient.setClient, #1567), so no extra Netty threads are created. The request customizer adds handshake headers and setConnectionInitPayload sets the connection_init payload. - Each operation is delivered by its own virtual thread, honoring demand. The protocol has no flow control: results received without demand are buffered up to maxResponseBytes per operation, beyond which the operation fails and the server is sent complete. - Errors before execution are a single result; a closed connection fails its operations with the close code; cancel() sends complete; stop() fails open operations, closes the connection and stops the WebSocket client, but not the borrowed HTTP client. - The WebSocket code is only loaded when the transport is selected. Refs #1586 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com>
The client signals the overflow at once, possibly before delivering the first buffered result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com>
Track subscribers from onSubscribe() so that stop() fails them all, signal non-positive demand without waiting for a result and complete the server operation, close the connection when its reader fails, send subscribe before any complete of a cancellation, signal setup failures, and count buffered results until they are delivered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jlouvel
force-pushed
the
feat/graphql-g4-websocket-client
branch
from
November 1, 2026 21:32
654387e to
c033658
Compare
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.





Closes #1586. Part of #1581.
Summary
subscribe()multiplexes operations over onegraphql-transport-wsconnection, opened on the first demand (withconnection_initand a 10 s acknowledgement timeout) and reopened if it was closed.WebSocketClientborrows the event loop of the adapter's HTTP client throughWebSocketClient.setClient(fix: let WebSocketClient share a borrowed client's event loop #1568), so the extension creates no Netty event loop group.stop()stops the WebSocket client but never the borrowed HTTP client.graphql-transport-wshas no flow control: results received without demand are buffered up tomaxResponseBytesper operation; beyond it the operation fails with aGraphqlTransportExceptionand the server is sentcomplete, which cancels the server-side subscription.errormessage) are a single result with errors, as with HTTP and SSE. A closed connection fails its operations with the close code and reason.cancel()sendscomplete.connection_initpayload carries tokens for servers that authenticate there.org.restlet.ext.websocketstays optional.Finding on #1568
WebSocketClient.setClientrequires the borrowed client to have aContext(it reads the helper from the context attributes).new Client(Protocol.HTTP)has none, so it can't be borrowed:new Client(new Context(), Protocol.HTTP)is needed. Worth a clearer error message or a fallback there.Validation
./mvnw -B verify -P default,test -pl org.restlet.ext.graphql(with core and the WebSocket extension from this branch): 63 tests, 0 failures; 91.6% instruction coverage (gate 80%); Spotless, SpotBugs, Javadoc,-Werrorand dependency convergence pass.New
GraphqlWebSocketClientTestCase(7 tests) againstGraphqlServerAdapter: a subscription and a query share one upgrade (counted by a filter) with demand honored, the init payload and a handshake header reaching data fetchers; cancelling one of two subscriptions cancels it on the server; adapter stop fails the other one and keeps the borrowed client; errors before execution as results; buffered results bounded, cancelling the server-side subscription; server shutdown (1001) fails subscriptions; endpoint without WebSocket; a borrowed client that isn't a started Netty client is rejected atstart().🤖 Generated with Claude Code